﻿using System;
using System.Data;
using System.Text;
using System.Collections;

namespace BizElements.Core
{
    /// <summary>
    /// An item retrieved by <see cref="SelectStatement"/>.
    /// </summary>
    [Serializable]
    public class SelectItem : ISqlItem, IRenderSql
    {
        #region Constructors.

        /// <summary>
        /// Creates an item.
        /// </summary>
        protected SelectItem()
        { 
        }

        private void Init(object item, SqlItemType itemType)
        {
            switch (itemType)
            {
                case SqlItemType.Column:
                    InitColumn((IDbColumn)item);
                    break;
                case SqlItemType.FreeText:
                    InitFreeText((string)item);
                    break;
                case SqlItemType.Function:
                    InitFunction((Function)item);
                    break;
                case SqlItemType.Parameter:
                    DbParameter par = (DbParameter)item;
                    InitParameter(par.Value, par.DbType, par.Size, par.ParameterName);
                    break;
                case SqlItemType.SubQuery:
                    InitQuery((SelectStatement)item);
                    break;
            }
        }

		/// <summary>
		/// Creates an item that contains a database field. Has a known data type and alias.
		/// </summary>
		/// <param name="column">Database column.</param>
		public SelectItem(IDbColumn column)
		{
			InitColumn(column);
		}

		private void InitColumn(IDbColumn column)
		{
			this.Item = column;
			this.ItemType = SqlItemType.Column;
			this.DataType = column.DataType;
			this.DbType = column.DbType;
			this.Alias = column.Alias;
		}

		/// <summary>
		/// Creates an item that contains SQL commands. Doesn't have a known data type and alias.
		/// </summary>
		/// <param name="freeText">SQL commands.</param>
		public SelectItem(string freeText)
		{
			InitFreeText(freeText);
		}

		private void InitFreeText(string freeText)
		{
			this.Item = freeText;
			this.ItemType = SqlItemType.FreeText;
		}

		/// <summary>
		/// Creates an item that contains SQL function. Has a known data type and alias.
		/// </summary>
		/// <param name="function">SQL function.</param>
		public SelectItem(Function function)
		{
			InitFunction(function);
		}

		private void InitFunction(Function function)
		{
			this.Item = function;
			this.ItemType = SqlItemType.Function;
			this.DataType = function.DataType;
			this.DbType = function.DbType;
			this.Alias = function.Alias;
		}

		/// <summary>
		/// Creates an item that contains an SQL parameter. Has a known data type. Doesn't assign alias.
		/// </summary>
		/// <param name="parameterValue">Parameter value.</param>
		/// <param name="parameterType">Parameter data type.</param>
		/// <param name="size">Size of the parameter. Required only for variable length types.</param>
		/// <param name="parameterNamePrefix">Parameter name prefix. Eg. "par_".</param>
		public SelectItem(object parameterValue, DbType parameterType, int size, string parameterNamePrefix)
		{
			InitParameter(parameterValue, parameterType, size, parameterNamePrefix);
		}		

		private void InitParameter(object parameterValue, DbType parameterType, int size, string parameterNamePrefix)
		{
			// Unique parameter ID is automatically appended to parameter name during the rendering phase.
			// Eg: 'parameter_15'. This is a safety precaution. It is possible that there are more
			// parameters with the same name.

			DbParameter dbpar = new DbParameter();
            dbpar.ParameterName = SqlItemUtil.CleanParameterName(parameterNamePrefix);
			dbpar.Direction = ParameterDirection.Input;
			dbpar.DbType = parameterType;
			dbpar.Value = parameterValue;
			if (size > 0)
				dbpar.Size = size;

			this.Item = dbpar;
			this.ItemType = SqlItemType.Parameter;
			this.DataType = TypeUtil.NetTypeFrom(parameterType);
			this.DbType = parameterType;
		}		

		/// <summary>
		/// Creates an item that contains a query. Assigns alias if query specifies one.
		/// If a query contains a single select item (field or function) than a data type will automatically be determined.
		/// </summary>
		/// <param name="query">Query.</param>
		public SelectItem(SelectStatement query)
		{
			InitQuery(query);
		}

		private void InitQuery(SelectStatement query)
		{
			this.Item = query;
			this.ItemType = SqlItemType.SubQuery;
            this.Alias = query.Alias;

            Type dataType;
            DbType dbType;
            SqlItemUtil.TryDetermineSubQueryDataType(query, out dataType, out dbType);
            this.DataType = dataType;
            this.DbType = dbType;			
		}        

		#endregion

        #region Properties and members.

        DbType dbType = DbType.Object;
        Type dataType = typeof(object);

        /// <summary>
        /// Gets contained item.
        /// </summary>
        public object Item { get; private set; }

        /// <summary>
        /// Gets item type.
        /// </summary>
        public SqlItemType ItemType { get; private set; }

        /// <summary>
        /// Gets the SQL data type. DbType.Object if unknown.
        /// </summary>
        public DbType DbType
        {
            get { return this.dbType; }
            private set { this.dbType = value; }
        }

        /// <summary>
        /// Gets .NET type. Object if unknown.
        /// </summary>
        public Type DataType
        {
            get { return this.dataType; }
            private set { this.dataType = value; }
        }

        /// <summary>
        /// Gets the item's alias/name. May be <b>null</b>.
        /// </summary>
        public string Alias { get; private set; }

        #endregion

        /// <summary>
        /// Copies members other item to current item.
        /// </summary>
        /// <param name="other">Other item.</param>
        protected void CopyFrom(SelectItem other)
        {
            Init(other.Item, other.ItemType);
        }

        #region Render.

        /// <summary>
        /// Renders select item as SQL statement.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            switch (this.ItemType)
            {
                case SqlItemType.Column:
                    RenderColumn(this.Item, dbms, output);
                    break;

                case SqlItemType.Parameter:
                    RenderParameter(this.Item, dbms, output, parameters);
                    break;

                case SqlItemType.SubQuery:
                    RenderSubQuery(this.Item, dbms, output, parameters);
                    break;

                case SqlItemType.FreeText:
                    RenderFreeText(this.Item, output);
                    break;

                case SqlItemType.Function:
                    RenderFunction(this.Item, dbms, output, parameters);
                    break;

                default:
                    throw new InvalidOperationException(Messages.SelectItem_UnsuportedItemType + this.ItemType.ToString());
            }
        }

        private static void RenderColumn(object item, DbmsType dbms, StringBuilder output)
        {
            IDbColumn col = (IDbColumn)item;
            if (col.ColumnName != col.Alias)
            {
                col.RenderFullName(dbms, output);
                output.Append(" AS ");
                col.RenderAlias(dbms, output);
            }
            else
            {
                col.RenderFullName(dbms, output);
            }
        }

        private static void RenderParameter(object item, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            DbParameter par = (DbParameter)item;
            par.Render(dbms, output, parameters);
        }

        private static void RenderSubQuery(object item, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            output.Append("(");
            SelectStatement select = (SelectStatement)item;
            select.Render(dbms, output, parameters);
            output.Append(")");
            if (select.Alias != null && select.Alias.Length > 0)
            {
                output.Append(" ");
                output.Append(select.Alias);
            }
        }

        private static void RenderFreeText(object item, StringBuilder output)
        {
            output.Append((string)item);
        }

        private static void RenderFunction(object item, DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            Function function = (Function)item;
            function.Render(dbms, output, parameters);
            if (function.Alias != null)
            {
                output.Append(" AS ");
                output.Append(function.Alias);
            }
        }

        #endregion
    }
}